<?php
/**
 * EasyAccount SDK
 * 
 * @author ShuangYa
 * @package EasyAccount
 * @category SDK
 * @link https://www.sylingd.com/
 * @copyright Copyright (c) 2017 ShuangYa
 * @license https://www.sylibs.com/go/easyaccount/license
 */

class EasyAccountException extends Exception {
}

class EasyAccount {
	const SDK_VERSION = '1.0';
	const API_VERSION = '1';

	const PROTOCOL_TCP = 1;
	const PROTOCOL_HTTP = 2;
	const PROTOCOL_UNIX = 3;
	/**
	 * EasyAccount will using Curl for HTTP protocol
	 * and using Swoole for TCP protocol and UNIX protocol
	 * If you want to using TCP or UNIX protocol, you must installed msgpack extension
	 */
	private $config = [
		'protocol' => self::PROTOCOL_TCP, // TCP or HTTP or UNIX
		'timeout' => 1, // need for HTTP and TCP
		'addr' => '127.0.0.1', // need for TCP and UNIX
		// UNIX Example: 'addr' => '/tmp/ea.sock',
		'url' => 'http://127.0.0.1:9501/', // need for HTTP
		'port' => '9502', // need for TCP
		/* EasyAccount App info */
		'id' => 1,
		'secret' => ''
	];

	private $connection = NULL;
	private $client_info = '';
	private $error = 0;

	private $cached_apps = [];

	public function __construct($config = []) {
		$this->config = array_merge($this->config, $config);
		if ($this->config['protocol'] === self::PROTOCOL_TCP) {
			$this->connection = new swoole_client(SWOOLE_TCP);
			if (!$this->connection->connect($this->config['addr'], $this->config['port'], $this->config['timeout'])) {
				throw new EasyAccountException('connect failed', $this->connection->errCode);
			}
		} elseif ($this->config['protocol'] === self::PROTOCOL_UNIX) {
			$this->connection = new swoole_client(SWOOLE_UNIX_STREAM);
			if (!$this->connection->connect($this->config['addr'], 0, $this->config['timeout'])) {
				throw new EasyAccountException('connect failed', $this->connection->errCode);
			}
		} else {
			$curl = curl_version();
			$this->client_info = 'EasyAccount/' . self::SDK_VERSION . ' PHP/' . PHP_VERSION . ' Curl/' . $curl['version'];
			if (isset($curl['ssl_version'])) {
				$this->client_info .= ' ' . $curl['ssl_version'];
			}
		}
	}
	public function __destruct() {
		if (($this->config['protocol'] === self::PROTOCOL_TCP || $this->config['protocol'] === self::PROTOCOL_UNIX) && is_object($this->connection)) {
			$this->connection->close();
		}
	}
	/**
	 * Fetch a url by curl
	 */
	private function fetchUrl($url, $data = []) {
		$ch = curl_init($url);
		if (isset($data['header'])) {
			curl_setopt($ch, CURLOPT_HTTPHEADER, $data['header']);
		}
		if (isset($data['post'])) {
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $data['post']);
		}
		if (substr($url, 0, 8) === 'https://') {
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
		}
		curl_setopt($ch, CURLOPT_TIMEOUT, isset($data['timeout']) ? $data['timeout'] : $this->config['timeout']);
		curl_setopt($ch, CURLOPT_USERAGENT, $this->client_info);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_HEADER, 0);
		$r = curl_exec($ch);
		if ($r === FALSE) {
			$this->error = curl_errno($ch);
		}
		@curl_close($ch);
		return $r;
	}
	/**
	 * Create a UUID
	 * @return string
	 */
	private static function generateUniqueId() {
		static $i = 0;
		$i == 0 && $i = mt_rand(1, 0x7FFFFF);
		return sprintf('%08x%06x%04x%06x',
			//4-byte value representing the seconds since the Unix epoch
			time() & 0xFFFFFFFF,
			//3-byte machine identifier
			crc32(substr((string)gethostname(), 0, 256)) >> 8 & 0xFFFFFF,
			//2-byte process id
			getmypid() & 0xFFFF,
			//3-byte counter, starting with a random value
			$i = $i > 0xFFFFFE ? 1 : $i + 1
		);
	}
	/**
	 * Call EasyAccount API
	 * @param string $method method name
	 * @param array $data
	 * @return array
	 */
	public function call($method, $data) {
		$data['_id'] = $this->config['id'];
		if ($this->config['protocol'] === self::PROTOCOL_HTTP) {
			$url = $this->config['url'] . 'apiv' . self::API_VERSION . '/' . str_replace('.', '/', $method);
			$data = $this->encodeData($data);
			$sign = $this->createSign($data);
			$rs = $this->fetchUrl($url, ['post' => $data, 'header' => ['x-sign: ' . $sign]]);
			if ($rs === FALSE) {
				throw new EasyAccountException('connect failed', $this->error);
			}
			$rs = json_decode($rs, 1);
		} else {
			$data['_version'] = self::API_VERSION;
			$data['_method'] = $method;
			$sendStr = $this->encodeData($data);
			try {
				$this->connection->send($sendStr);
				$rs = $this->connection->recv();
			} catch (\Exception $e) {
				throw new EasyAccountException('connect failed', $e->errorInfo());
			}
			$rs = substr($rs, 4);
			$rs = msgpack_unpack($rs);
		}
		return $rs;
	}
	/**
	 * Encode an array to a string
	 * @param array $data
	 * @param int $type Encode type
	 * @return string
	 */
	public function encodeData($data, $type = NULL) {
		if ($type === NULL) {
			$type = $this->config['protocol'];
		}
		if (!isset($data['_uuid'])) {
			$data['_uuid'] = self::generateUniqueId();
		}
		if ($type === self::PROTOCOL_HTTP) {
			ksort($data);
			return json_encode($data);
		} else {
			ksort($data);
			$data['_sign'] = $this->createSign(http_build_query($data));
			$sendStr = msgpack_pack($data);
			$sendStr = pack('N', strlen($sendStr)) . $sendStr;
			return $sendStr;
		}
	}
	/**
	 * Create a signature string
	 * @param string $data
	 * @return string
	 */
	public function createSign($data) {
		return hash_hmac('md5', $data, $this->config['secret']);
	}
	/**
	 * Send a heartbeat package
	 */
	public function heartbeat() {
		$sendStr = 'heartbeat';
		$sendStr = pack('N', strlen($sendStr)) . $sendStr;
		$this->connection->send($sendStr);
		$this->connection->recv();
	}
	/**
	 * Check a signature string sent by another app is valid or not
	 * Require app.get permission
	 * @param string $sign Signature string sent by another app
	 * @param int $appid App ID
	 * @param string $data Data sent by another app
	 * @return boolean
	 */
	public function checkAppSign($sign, $appid, $data) {
		if (!isset($this->cached_apps[$appid])) {
			$app = $this->call('app.get', ['id' => $appid]);
			if (!isset($app['secret'])) {
				$this->cached_apps[$appid] = NULL;
				return FALSE;
			}
			$this->cached_apps[$appid] = $app;
		} else {
			if ($this->cached_apps[$appid] === NULL) {
				return FALSE;
			}
			$app = $this->cached_apps[$appid];
		}
		return hash_hmac('md5', $data, $app['secret']) === $sign;
	}
}
